package org.giorgiocalderolla.showrssx;

import java.awt.Desktop;
import java.awt.FileDialog;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Menu;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Observable;
import java.util.Observer;

import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;

import org.giorgiocalderolla.showrssx.Configuration.ConfigurationException;
import org.giorgiocalderolla.showrssx.Configuration.Status;

/**
 * @author Giorgio Calderolla
 * 
 *         Provides UI for the menubar icon and for preferences
 */
public class GUI implements ActionListener, Observer, ClipboardOwner {

	// UI code is boring

	// UI components
	private JFrame preferencesFrame = null;
	private JFrame problemFrame = null;
	private ShowRSSX controller = null;
	private Configuration configuration = null;
	private JTextField urlText = null;
	private JTextArea problemText = null;
	private JComboBox urlUpdateInterval = null;
	private JTextField folderText = null;
	private MenuItem status = null;
	private Menu recent = null;
	private Menu problems = null;
	private MenuItem openFolder = null;
	private MenuItem pauseResume = null;
	private TrayIcon trayIcon = null;

	// UI images
	private BufferedImage icon = null;
	private BufferedImage iconDisabled = null;
	private BufferedImage iconRefreshing = null;

	// UI strings
	private final String MENU_FOLDER = "Show in Finder";
	private final String MENU_WEBSITE = "Launch ShowRSS Website";
	private final String MENU_PAUSE = "Pause";
	private final String MENU_RESUME = "Resume";
	private final String MENU_STATUS_PREFIX = "Last update: ";
	private final String MENU_RECENTLY_DOWNLOADED = "Recently Downloaded Torrents";
	private final String MENU_PROBLEMS = "Problems";
	private final String MENU_HELP = "Help";
	private final String MENU_HELP_WEBSITE = "Visit the website";
	private final String MENU_HELP_CONFIGURATION = "Help me configure "
			+ ShowRSSX.APPLICATION_NAME;
	private final String MENU_HELP_FEATURE_REQUEST = "Request a feature";
	private final String MENU_HELP_BUG = "Report a problem with "
			+ ShowRSSX.APPLICATION_NAME;
	private final String MENU_HELP_VERSION = ShowRSSX.APPLICATION_NAME + " "
			+ ShowRSSX.APPLICATION_VERSION;
	private final String MENU_PREFERENCES = "Preferences...";
	private final String MENU_QUIT = "Quit " + ShowRSSX.APPLICATION_NAME;

	private final String PREFERENCES_TITLE = ShowRSSX.APPLICATION_NAME
			+ " Preferences";
	private final String PREFERENCES_FEED_URL = "ShowRSS Feed URL";
	private final String PREFERENCES_TORRENT_FOLDER = "Torrent folder";
	private final String PREFERENCES_TORRENT_FOLDER_BROWSE = "Browse...";
	private final String PREFERENCES_HELP = MENU_HELP_CONFIGURATION;
	private final String PREFERENCES_OK = "OK";

	private final String PROBLEM_TITLE = "Problem Reporting";
	private final String PROBLEM_REPORT = "Report this problem";
	private final String PROBLEM_DONE = "Done";
	private final String PROBLEM_COMMAND_PREFIX = "PROBLEM_REPORT_";

	private final String ACTION_PASTE = "Paste";

	/**
	 * Sole constructor
	 * 
	 * @param controller
	 *            the application object
	 * @param configuration
	 *            the configuration manager
	 * @throws Exception
	 *             if the creation of the GUI fails
	 */
	public GUI(ShowRSSX controller, Configuration configuration) {

		this.controller = controller;
		this.configuration = configuration;

		// load images
		try {
			icon = ImageIO.read(getClass().getResourceAsStream(
					"/res/showrssx.png"));
			iconDisabled = ImageIO.read(getClass().getResourceAsStream(
					"/res/showrssx-disabled.png"));
			iconRefreshing = ImageIO.read(getClass().getResourceAsStream(
					"/res/showrssx-refreshing.png"));
		} catch (Exception e) {
			// we really need those images!
			fatalError("Couldn't load images: " + e.getLocalizedMessage());
		}

		// build UI
		try {
			createGUI();
		} catch (Exception e) {
			// we cannot do much without the GUI
			fatalError("UI creation failed: " + e.getLocalizedMessage());
		}
	}

	/**
	 * Creates the GUI (menubar icon, preferences dialog)
	 * 
	 * @throws Exception
	 *             if the creation of the GUI fails for any reason
	 */
	private void createGUI() throws Exception {

		UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

		createMenu();

		createPreferencesWindow();

		createProblemReportingWindow();

		updateUI(null);
	}

	/**
	 * Creates the main application interface, that is the menubar icon and
	 * related popup menu
	 * 
	 * @throws Exception
	 *             if anything goes wrong
	 */
	private void createMenu() throws Exception {

		// create the tray icon
		trayIcon = new TrayIcon(this.iconDisabled);
		SystemTray tray = SystemTray.getSystemTray();
		tray.add(trayIcon);

		// create the popup menu and its items
		PopupMenu popup = new PopupMenu();
		MenuItem launchWebsite = new MenuItem(MENU_WEBSITE);
		launchWebsite.addActionListener(this);

		pauseResume = new MenuItem(MENU_PAUSE);
		pauseResume.setEnabled(false);
		pauseResume.addActionListener(this);
		status = new MenuItem(MENU_STATUS_PREFIX);
		status.setEnabled(false);
		recent = new Menu(MENU_RECENTLY_DOWNLOADED);
		problems = new Menu(MENU_PROBLEMS);
		problems.setEnabled(false);
		openFolder = new MenuItem(MENU_FOLDER);
		openFolder.addActionListener(this);

		Menu help = new Menu(MENU_HELP);
		MenuItem website = new MenuItem(MENU_HELP_WEBSITE);
		website.addActionListener(this);
		MenuItem howTo = new MenuItem(MENU_HELP_CONFIGURATION);
		howTo.addActionListener(this);
		MenuItem feature = new MenuItem(MENU_HELP_FEATURE_REQUEST);
		feature.addActionListener(this);
		MenuItem bug = new MenuItem(MENU_HELP_BUG);
		bug.addActionListener(this);
		MenuItem version = new MenuItem(MENU_HELP_VERSION);
		version.setEnabled(false);
		help.add(website);
		help.add(howTo);
		help.add(feature);
		help.add(bug);
		help.add(version);

		MenuItem preferences = new MenuItem(MENU_PREFERENCES);
		preferences.addActionListener(this);
		MenuItem quit = new MenuItem(MENU_QUIT);
		quit.addActionListener(this);

		// put everything together
		popup.add(launchWebsite);
		popup.addSeparator();
		popup.add(recent);
		popup.add(status);
		popup.add(problems);
		popup.add(pauseResume);
		popup.addSeparator();
		popup.add(help);
		popup.add(preferences);
		popup.add(quit);
		trayIcon.setPopupMenu(popup);
	}

	/**
	 * Creates the preferences window for application setup
	 */
	private void createPreferencesWindow() {

		// create the preferences window
		preferencesFrame = new JFrame(PREFERENCES_TITLE);
		preferencesFrame.setResizable(true);
		JPanel rootPanel = new JPanel();
		rootPanel.setLayout(new BoxLayout(rootPanel, BoxLayout.PAGE_AXIS));
		rootPanel.setBorder(new EmptyBorder(10, 10, 10, 10));

		JPanel urlPanel = new JPanel();
		urlPanel.setLayout(new BoxLayout(urlPanel, BoxLayout.PAGE_AXIS));
		urlPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory
				.createTitledBorder(PREFERENCES_FEED_URL), BorderFactory
				.createEmptyBorder(3, 3, 3, 3)));
		JPanel folderPanel = new JPanel(new GridBagLayout());
		folderPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory
				.createTitledBorder(PREFERENCES_TORRENT_FOLDER), BorderFactory
				.createEmptyBorder(3, 3, 3, 3)));
		JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
		rootPanel.add(urlPanel);
		rootPanel.add(folderPanel);
		rootPanel.add(buttonsPanel);

		// add inputs for the feed URL
		urlText = new JTextField(45);
		urlText.setText(configuration.getFeedUrl());
		JPopupMenu pasteMenu = new JPopupMenu();
		JMenuItem paste = new JMenuItem(ACTION_PASTE);
		paste.addActionListener(this);
		pasteMenu.add(paste);
		urlText.setComponentPopupMenu(pasteMenu);

		JPanel urlUpdateIntervalPanel = new JPanel(new FlowLayout(
				FlowLayout.CENTER));
		JLabel urlUpdateLabel1 = new JLabel("Look for new torrents every");
		urlUpdateInterval = new JComboBox();
		for (int allowedUpdateInterval : Configuration.ALLOWED_UPDATE_INTERVALS) {
			urlUpdateInterval.addItem(new Integer(allowedUpdateInterval));
		}
		for (int i = 0; i < Configuration.ALLOWED_UPDATE_INTERVALS.length; i++) {
			if (configuration.getUpdateInterval() == Configuration.ALLOWED_UPDATE_INTERVALS[i]) {
				urlUpdateInterval.setSelectedIndex(i);
			}
		}
		JLabel urlUpdateLabel2 = new JLabel("minutes");
		urlUpdateIntervalPanel.add(urlUpdateLabel1);
		urlUpdateIntervalPanel.add(urlUpdateInterval);
		urlUpdateIntervalPanel.add(urlUpdateLabel2);

		urlPanel.add(urlText);
		urlPanel.add(urlUpdateIntervalPanel);

		// add inputs for the torrent folder and a browse button
		GridBagConstraints layoutConstraints = new GridBagConstraints();
		folderText = new JTextField(40);
		folderText.setEditable(false);
		folderText.setBorder(BorderFactory.createEtchedBorder());
		folderText.setText(configuration.getTorrentFolder());
		layoutConstraints.fill = GridBagConstraints.HORIZONTAL;
		layoutConstraints.gridx = 0;
		layoutConstraints.gridy = 0;
		folderPanel.add(folderText);
		JButton browse = new JButton(PREFERENCES_TORRENT_FOLDER_BROWSE);
		browse.addActionListener(this);
		layoutConstraints.fill = GridBagConstraints.NONE;
		layoutConstraints.gridx = 1;
		folderPanel.add(browse);

		// add the Help button
		JButton help = new JButton(PREFERENCES_HELP);
		help.addActionListener(this);
		buttonsPanel.add(help);

		// add the OK button
		JButton ok = new JButton(PREFERENCES_OK);
		ok.addActionListener(this);
		buttonsPanel.add(ok);

		preferencesFrame.add(rootPanel);
		preferencesFrame.pack();

		// place at the centre of the screen
		preferencesFrame.setLocationRelativeTo(null);
	}

	/**
	 * Creates a dialog for easy error reporting
	 */
	private void createProblemReportingWindow() {

		// create the preferences window
		problemFrame = new JFrame(PROBLEM_TITLE);
		problemFrame.setResizable(true);
		JPanel rootPanel = new JPanel();
		rootPanel.setLayout(new BoxLayout(rootPanel, BoxLayout.PAGE_AXIS));
		rootPanel.setBorder(new EmptyBorder(10, 10, 10, 10));

		// label (to explain what's going on)
		JLabel problemLabel = new JLabel(
				"The text below has been copied to your clipboard. "
						+ "Please paste it on the bug report form. Thank you!");
		rootPanel.add(problemLabel);

		// text area for problem
		problemText = new JTextArea(20, 50);
		problemText.setEditable(false);
		JScrollPane scrollPane = new JScrollPane(problemText);
		rootPanel.add(scrollPane);

		// buttons
		JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
		rootPanel.add(buttonsPanel);

		// add the Report button
		JButton report = new JButton(PROBLEM_REPORT);
		report.addActionListener(this);
		buttonsPanel.add(report);

		// add the Done button
		JButton done = new JButton(PROBLEM_DONE);
		done.addActionListener(this);
		buttonsPanel.add(done);

		problemFrame.add(rootPanel);
		problemFrame.pack();

		// place at the centre of the screen
		problemFrame.setLocationRelativeTo(null);
	}

	/**
	 * Updates the UI with status information
	 * 
	 * @param changedPreference
	 *            the preference describing the status that has changed, null to
	 *            update all
	 */
	private synchronized void updateUI(Object changedPreference) {

		boolean updateAll = changedPreference == null ? true : false;

		// we need a proper configuration
		if (!configuration.isConfigured())
			return;

		// update the status if needed
		if ((changedPreference == Configuration.PREFERENCE_LAST_UPDATE_TIME)
				|| (changedPreference == Configuration.PREFERENCE_LAST_UPDATE_FAILED)
				|| updateAll) {
			status.setLabel(MENU_STATUS_PREFIX
					+ configuration.getLastUpdateTime()
					+ (configuration.isLastUpdateFailed() ? " (failed)" : ""));
		}

		// if the last update time has changed, we might need to
		// update the recent torrent list
		if ((changedPreference == Configuration.PREFERENCE_LAST_UPDATE_TIME)
				|| updateAll) {
			recent.removeAll();
			int recentAdded = 0;
			for (String torrent : configuration.getDownloadedTorrents()) {
				try {
					String filename = FeedChecker
							.urlToFilename(new URL(torrent));
					MenuItem item = new MenuItem((recentAdded + 1) + ") "
							+ filename);
					item.setEnabled(false);
					recent.add(item);
					recentAdded++;
				} catch (MalformedURLException mue) {
					// this shouldn't happen
				} catch (IOException ioe) {
					// this shouldn't either
				}

				// let's just show 10 elements
				if (recentAdded == 10) {
					break;
				}
			}

			if (recentAdded == 0) {
				MenuItem item = new MenuItem("No torrents yet!");
				item.setEnabled(false);
				recent.add(item);
			}

			recent.add(openFolder);
		}

		// new problem, update the problems list
		if ((changedPreference == Configuration.PREFERENCE_PROBLEMS)
				|| updateAll) {
			problems.removeAll();
			int problemsAdded = 0;
			for (Problem problem : configuration.getProblems()) {
				MenuItem item = new MenuItem((problemsAdded + 1) + ") "
						+ problem.getMessage());
				item.setActionCommand(PROBLEM_COMMAND_PREFIX + problemsAdded);
				item.addActionListener(this);
				problems.add(item);
				problemsAdded++;

				// let's just show 10 elements
				if (problemsAdded == 10) {
					break;
				}
			}

			if (problemsAdded == 0) {
				problems.setEnabled(false);
			} else {
				MenuItem item = new MenuItem(
						"Please click on a problem to report it!");
				item.setEnabled(false);
				problems.add(item);
				problems.setEnabled(true);
			}
		}

		// status change: update tray icon accordingly
		if ((changedPreference == Configuration.PREFERENCE_STATUS) || updateAll) {
			Status newStatus = configuration.getCurrentStatus();

			switch (newStatus) {

			case DISABLED:
				// set disabled icon
				trayIcon.setImage(iconDisabled);

				break;

			case ACTIVE_IDLE:
				// set idle icon
				trayIcon.setImage(icon);

				break;

			case ACTIVE_WORKING:
				// set idle icon
				trayIcon.setImage(iconRefreshing);

				break;
			}
		}

		// configuration change: the app is now configured
		if ((changedPreference == Configuration.PREFERENCE_CONFIGURED)
				|| updateAll) {
			pauseResume.setEnabled(true);
		}
	}

	/**
	 * Displays a modal error dialog and quit the application
	 * 
	 * @param message
	 *            the error message to display
	 */
	public static void fatalError(String message) {

		JOptionPane.showMessageDialog(null, message, "Fatal Error",
				JOptionPane.ERROR_MESSAGE);
		ShowRSSX.abort();
	}

	/**
	 * Displays a modal error dialog
	 * 
	 * @param message
	 *            the error message to display
	 */
	public static void loudError(String message) {

		JOptionPane.showMessageDialog(null, message, "Error",
				JOptionPane.ERROR_MESSAGE);
	}

	/**
	 * Responds to UI events
	 * 
	 * @param event
	 *            the event to be acted upon
	 */
	@Override
	public synchronized void actionPerformed(ActionEvent event) {

		String command = event.getActionCommand();

		if (command == MENU_FOLDER) {
			// open the torrent folder
			try {
				Desktop.getDesktop().open(
						new File(configuration.getTorrentFolder()));
			} catch (Exception e) {
				loudError("Couldn't open torrent folder: "
						+ e.getLocalizedMessage());
			}
		} else if (command == MENU_PREFERENCES) {
			// show the preferences dialog
			showPreferences();
		} else if (command == PREFERENCES_OK) {
			// save preferences and close dialog
			if (savePreferences()) {
				preferencesFrame.setVisible(false);
			}
		} else if (command == PREFERENCES_TORRENT_FOLDER_BROWSE) {
			// browse for the directory
			System.setProperty("apple.awt.fileDialogForDirectories", "true");
			FileDialog browseDialog = new FileDialog(preferencesFrame,
					"Select the torrent folder", FileDialog.LOAD);
			browseDialog.setDirectory(configuration.getTorrentFolder());
			browseDialog.setLocationRelativeTo(null);
			browseDialog.setVisible(true); // this opens a modal dialog
			folderText.setText(browseDialog.getDirectory()
					+ browseDialog.getFile());
			System.setProperty("apple.awt.fileDialogForDirectories", "false");
		} else if ((command == PREFERENCES_HELP)
				|| (command == MENU_HELP_CONFIGURATION)) {
			// launch the wiki page for configuration
			browse(ShowRSSX.APPLICATION_CONFIGURATION_URL);
		} else if (command == MENU_WEBSITE) {
			// launch showrss website
			browse(ShowRSSX.SERVICE_WEBSITE_URL);
		} else if ((command == MENU_HELP_BUG) || (command == PROBLEM_REPORT)) {
			// launch the bug reporting page
			browse(ShowRSSX.APPLICATION_BUG_REPORT_URL);
		} else if ((command == MENU_HELP_FEATURE_REQUEST)) {
			// launch the feature request page
			browse(ShowRSSX.APPLICATION_FEATURE_REQUEST_URL);
		} else if ((command == MENU_HELP_WEBSITE)) {
			// launch the website for ShowRSSX
			browse(ShowRSSX.APPLICATION_WEBSITE_URL);
		} else if ((command == MENU_PAUSE) || (command == MENU_RESUME)) {
			// suspend/resume feed checking
			if ((configuration.getCurrentStatus() == Status.ACTIVE_IDLE)
					|| (configuration.getCurrentStatus() == Status.ACTIVE_WORKING)) {
				controller.pause();
				pauseResume.setLabel(MENU_RESUME);
			} else {
				controller.resume();
				pauseResume.setLabel(MENU_PAUSE);
			}
		} else if (command == ACTION_PASTE) {
			// paste from the clipboard to the url feed text field
			urlText.paste();
		} else if (command == MENU_QUIT) {
			// cleanly quit
			controller.quit();
		} else if (command.startsWith(PROBLEM_COMMAND_PREFIX)) {
			// problem reporting
			int problemIndex = Integer.parseInt(command
					.substring(PROBLEM_COMMAND_PREFIX.length()));
			String problem = configuration.getProblems().get(problemIndex)
					.toString();
			problemText.setText(problem);
			problemText.setCaretPosition(0);
			Toolkit.getDefaultToolkit().getSystemClipboard().setContents(
					new StringSelection(problem), this);
			showProblemReporting();
		} else if (command == PROBLEM_DONE) {
			// close dialog
			problemFrame.setVisible(false);
		}
	}

	/**
	 * Convenience method for web browser launching
	 * 
	 * @param url
	 *            the web address to open in the browser
	 */
	public static void browse(String url) {
		try {
			Desktop.getDesktop().browse(new URI(url));
		} catch (Exception e) {
			loudError("Couldn't launch browser: " + e.getLocalizedMessage());
		}
	}

	/**
	 * Displays a preferences dialog allowing the user to setup the application
	 */
	public void showPreferences() {

		preferencesFrame.setAlwaysOnTop(true);
		preferencesFrame.setVisible(true);
		preferencesFrame.setAlwaysOnTop(false);
	}

	/**
	 * Displays a problem reporting dialog allowing the user to report issues
	 */
	public void showProblemReporting() {

		problemFrame.setAlwaysOnTop(true);
		problemFrame.setVisible(true);
		problemFrame.setAlwaysOnTop(false);
	}

	/**
	 * Reads user preferences and save them using the configuration manager
	 * 
	 * @return true if the chosen preferences are valid, false otherwise
	 */
	private boolean savePreferences() {

		// read the settings, forgiving whitespace
		String feedUrl = urlText.getText().trim();
		String torrentFolder = folderText.getText().trim();
		int updateInterval = Configuration.ALLOWED_UPDATE_INTERVALS[urlUpdateInterval
				.getSelectedIndex()];

		// save the values
		synchronized (configuration) {
			boolean goodValues = true;
			try {
				configuration.setFeedUrl(feedUrl);
			} catch (ConfigurationException ce) {
				loudError("The chosen Feed URL is not valid: "
						+ ce.getCause().getLocalizedMessage());
				goodValues = false;
			}

			try {
				configuration.setTorrentFolder(torrentFolder);
			} catch (ConfigurationException ce) {
				loudError("The chosen Torrent Folder cannot be used: "
						+ ce.getCause().getLocalizedMessage());
				goodValues = false;
			}

			try {
				configuration.setUpdateInterval(updateInterval);
			} catch (ConfigurationException ce) {
				loudError("The chosen Update Frequency cannot be used: "
						+ ce.getCause().getLocalizedMessage());
				goodValues = false;
			}

			if (goodValues) {
				// the application can now be considered as properly configured
				configuration.setConfigured();
				return true;
			} else
				return false;
		}
	}

	/**
	 * This is called whenever an observed object changes. We are only
	 * interested in Configuration changes
	 */
	@Override
	public void update(Observable observable, Object changedPreference) {

		// has the configuration changed? is it in a valid state?
		if ((observable == configuration) && configuration.isConfigured()) {
			// update what may be changed
			updateUI(changedPreference);
		}
	}

	/**
	 * This is called whenever we lose ownership of the clipboard
	 */
	@Override
	public void lostOwnership(Clipboard clipboard, Transferable contents) {
		// I don't care about this event
	}
}
